Необходимо провести исследование клиентов банка и выделить сегменты, которые склонны уходить из банка.
Предоставлен датафрейм с данными клиентов банка.
Структура данных:
!pip3 install sidetable
!pip3 install phik
!pip3 install missingno
Requirement already satisfied: sidetable in /opt/conda/lib/python3.9/site-packages (0.9.1) Requirement already satisfied: pandas>=1.0 in /opt/conda/lib/python3.9/site-packages (from sidetable) (1.2.4) Requirement already satisfied: python-dateutil>=2.7.3 in /opt/conda/lib/python3.9/site-packages (from pandas>=1.0->sidetable) (2.8.1) Requirement already satisfied: pytz>=2017.3 in /opt/conda/lib/python3.9/site-packages (from pandas>=1.0->sidetable) (2021.1) Requirement already satisfied: numpy>=1.16.5 in /opt/conda/lib/python3.9/site-packages (from pandas>=1.0->sidetable) (1.21.1) Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.9/site-packages (from python-dateutil>=2.7.3->pandas>=1.0->sidetable) (1.16.0) Requirement already satisfied: phik in /opt/conda/lib/python3.9/site-packages (0.12.3) Requirement already satisfied: joblib>=0.14.1 in /opt/conda/lib/python3.9/site-packages (from phik) (1.1.0) Requirement already satisfied: pandas>=0.25.1 in /opt/conda/lib/python3.9/site-packages (from phik) (1.2.4) Requirement already satisfied: matplotlib>=2.2.3 in /opt/conda/lib/python3.9/site-packages (from phik) (3.3.4) Requirement already satisfied: scipy>=1.5.2 in /opt/conda/lib/python3.9/site-packages (from phik) (1.9.1) Requirement already satisfied: numpy>=1.18.0 in /opt/conda/lib/python3.9/site-packages (from phik) (1.21.1) Requirement already satisfied: cycler>=0.10 in /opt/conda/lib/python3.9/site-packages (from matplotlib>=2.2.3->phik) (0.11.0) Requirement already satisfied: python-dateutil>=2.1 in /opt/conda/lib/python3.9/site-packages (from matplotlib>=2.2.3->phik) (2.8.1) Requirement already satisfied: kiwisolver>=1.0.1 in /opt/conda/lib/python3.9/site-packages (from matplotlib>=2.2.3->phik) (1.4.4) Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.3 in /opt/conda/lib/python3.9/site-packages (from matplotlib>=2.2.3->phik) (2.4.7) Requirement already satisfied: pillow>=6.2.0 in /opt/conda/lib/python3.9/site-packages (from matplotlib>=2.2.3->phik) (8.4.0) Requirement already satisfied: pytz>=2017.3 in /opt/conda/lib/python3.9/site-packages (from pandas>=0.25.1->phik) (2021.1) Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.9/site-packages (from python-dateutil>=2.1->matplotlib>=2.2.3->phik) (1.16.0) Requirement already satisfied: missingno in /opt/conda/lib/python3.9/site-packages (0.5.2) Requirement already satisfied: numpy in /opt/conda/lib/python3.9/site-packages (from missingno) (1.21.1) Requirement already satisfied: seaborn in /opt/conda/lib/python3.9/site-packages (from missingno) (0.11.1) Requirement already satisfied: scipy in /opt/conda/lib/python3.9/site-packages (from missingno) (1.9.1) Requirement already satisfied: matplotlib in /opt/conda/lib/python3.9/site-packages (from missingno) (3.3.4) Requirement already satisfied: python-dateutil>=2.1 in /opt/conda/lib/python3.9/site-packages (from matplotlib->missingno) (2.8.1) Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.3 in /opt/conda/lib/python3.9/site-packages (from matplotlib->missingno) (2.4.7) Requirement already satisfied: kiwisolver>=1.0.1 in /opt/conda/lib/python3.9/site-packages (from matplotlib->missingno) (1.4.4) Requirement already satisfied: cycler>=0.10 in /opt/conda/lib/python3.9/site-packages (from matplotlib->missingno) (0.11.0) Requirement already satisfied: pillow>=6.2.0 in /opt/conda/lib/python3.9/site-packages (from matplotlib->missingno) (8.4.0) Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.9/site-packages (from python-dateutil>=2.1->matplotlib->missingno) (1.16.0) Requirement already satisfied: pandas>=0.23 in /opt/conda/lib/python3.9/site-packages (from seaborn->missingno) (1.2.4) Requirement already satisfied: pytz>=2017.3 in /opt/conda/lib/python3.9/site-packages (from pandas>=0.23->seaborn->missingno) (2021.1)
import pandas as pd
import numpy as np
import os
import seaborn as sns
import matplotlib.pyplot as plt
from pandas.plotting import register_matplotlib_converters
import warnings
register_matplotlib_converters()
import plotly.express as px
from plotly import graph_objects as go
import math as mth
import sidetable as stb
from scipy import stats as st
import phik
import missingno as msno
pth_1 = '/datasets/bank_scrooge.csv'
pth_2 = 'https://code.s3.yandex.net/datasets/bank_scrooge.csv'
if os.path.exists(pth_1):
df = pd.read_csv(pth_1)
elif os.path.exists(pth_2):
df = pd.read_csv(pth_2)
else:
print('Проверьте правильность пути к датафрейму')
df.head()
| USERID | score | city | gender | age | equity | balance | products | credit_card | last_activity | EST_SALARY | churn | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 183012 | 850.0 | Рыбинск | Ж | 25.0 | 1 | 59214.82 | 2 | 0 | 1 | 75719.14 | 1 |
| 1 | 146556 | 861.0 | Рыбинск | Ж | 37.0 | 5 | 850594.33 | 3 | 1 | 0 | 86621.77 | 0 |
| 2 | 120722 | 892.0 | Рыбинск | Ж | 30.0 | 0 | NaN | 1 | 1 | 1 | 107683.34 | 0 |
| 3 | 225363 | 866.0 | Ярославль | Ж | 51.0 | 5 | 1524746.26 | 2 | 0 | 1 | 174423.53 | 1 |
| 4 | 157978 | 730.0 | Ярославль | М | 34.0 | 5 | 174.00 | 1 | 1 | 0 | 67353.16 | 1 |
Названия ряда столбцов не соответствуют стилистике pandas.
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 10000 entries, 0 to 9999 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 USERID 10000 non-null int64 1 score 10000 non-null float64 2 city 10000 non-null object 3 gender 10000 non-null object 4 age 9974 non-null float64 5 equity 10000 non-null int64 6 balance 7705 non-null float64 7 products 10000 non-null int64 8 credit_card 10000 non-null int64 9 last_activity 10000 non-null int64 10 EST_SALARY 10000 non-null float64 11 churn 10000 non-null int64 dtypes: float64(4), int64(6), object(2) memory usage: 937.6+ KB
Датафрейм содержит 12 столбцов и 10 000 строк.
В некоторых столбцах есть пропуски.
Названия столбцов USERID, EST_SALARY не соответствуют стилистике пандас.
Тип данных у 6 столбцов (int64), у 4-х столбцов (float64), у 2-х (object).
Отметим, что у столбца age (и, возможно, столбца score) с целочисленными значениями тип данных float64.
Приведем все названия столбцов к нижнему регистру и добавим змеиный регистр к столбцу с идентификатором клиента.
df.columns = df.columns.str.lower()
df.rename(columns={'userid':'user_id'}, inplace=True)
list(df)
['user_id', 'score', 'city', 'gender', 'age', 'equity', 'balance', 'products', 'credit_card', 'last_activity', 'est_salary', 'churn']
После внесенных изменений названия столбцов соответствуют стилистике pandas.
Проверим наличие отрицательных значений в столбцах с числовыми значениями.
Cоздадим функцию, которая в каждом столбце с числовыми значениями будет находить количество отрицательных значений.
def minus_values(df):
columns_num = (
df.select_dtypes(include=['int64']).columns.tolist()
+ df.select_dtypes(include=['float64']).columns.tolist()
)
for column in columns_num:
print('Количество отрицательных значений в {}'.format(column),
df[df[column] < 0][column].count())
minus_values(df)
Количество отрицательных значений в user_id 0 Количество отрицательных значений в equity 0 Количество отрицательных значений в products 0 Количество отрицательных значений в credit_card 0 Количество отрицательных значений в last_activity 0 Количество отрицательных значений в churn 0 Количество отрицательных значений в score 0 Количество отрицательных значений в age 0 Количество отрицательных значений в balance 0 Количество отрицательных значений в est_salary 0
Отрицательные значения отсутствуют.
Посмотрим уникальные значения в столбцах city, gender, credit_card, last_activity, churn.
column_uniq = ['city', 'gender', 'credit_card', 'last_activity', 'churn']
for column in column_uniq:
print('Уникальные значения в столбце {}'.format(column),
df[column].unique().tolist())
Уникальные значения в столбце city ['Рыбинск', 'Ярославль', 'Ростов'] Уникальные значения в столбце gender ['Ж', 'М'] Уникальные значения в столбце credit_card [0, 1] Уникальные значения в столбце last_activity [1, 0] Уникальные значения в столбце churn [1, 0]
В столбцах находятся только значения в соответствии с описанием данных датафрейм.
df.stb.missing()
| missing | total | percent | |
|---|---|---|---|
| balance | 2295 | 10000 | 22.95 |
| age | 26 | 10000 | 0.26 |
| user_id | 0 | 10000 | 0.00 |
| score | 0 | 10000 | 0.00 |
| city | 0 | 10000 | 0.00 |
| gender | 0 | 10000 | 0.00 |
| equity | 0 | 10000 | 0.00 |
| products | 0 | 10000 | 0.00 |
| credit_card | 0 | 10000 | 0.00 |
| last_activity | 0 | 10000 | 0.00 |
| est_salary | 0 | 10000 | 0.00 |
| churn | 0 | 10000 | 0.00 |
В столбцах balance и age присутствуют пропуски, доля которых соответственно составляет 22.95% и 0.26%.
Построим матрицу пропущенных значений, чтобы посмотреть наличие в пропусках какой-либо закономерности.
msno.matrix(df, color=(0.27, 0.52, 1.0));
plt.title('Распределение пропущенных значений по датафрейму', fontsize=25, fontweight="bold")
plt.xlabel('Наименование столбцов', fontsize=20, fontweight="bold")
plt.ylabel('Строки', fontsize=20, fontweight="bold");
plt.show();
Распределение пропущенных значений в датафрейме выглядит случайным:
Построим матрицу корреляции пропущенных значений, которая показывает насколько сильно присутствие или отсутствие значений одного признака влияет на присутствие значений другого.
round(df[['age', 'balance']].isna().corr(), 3)
| age | balance | |
|---|---|---|
| age | 1.000 | 0.047 |
| balance | 0.047 | 1.000 |
Корреляция пропусков близка к нулю, т.е. пропуски одного признака не влияют на пропуски другого.
Поскольку исследование проводится на предмет оттока клиентов, посмотрим количество ушедших клиентов в столбцах age и balance которых отсутствуют значения.
print('количество ушедших клиентов в столбце c пропусками age', \
len(df.query('age.isna() == True and churn == 1')))
print('количество ушедших клиентов в столбце c пропусками balance', \
len(df.query('balance.isna() == True and churn == 1')))
количество ушедших клиентов в столбце c пропусками age 1 количество ушедших клиентов в столбце c пропусками balance 13
Обработаем пропуски следующим образом:
Удалим строки с пропусками в столбце age и сбросим индексы.
df.dropna(subset = ['age'], inplace = True)
df = df.reset_index(drop=True)
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 9974 entries, 0 to 9973 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 9974 non-null int64 1 score 9974 non-null float64 2 city 9974 non-null object 3 gender 9974 non-null object 4 age 9974 non-null float64 5 equity 9974 non-null int64 6 balance 7695 non-null float64 7 products 9974 non-null int64 8 credit_card 9974 non-null int64 9 last_activity 9974 non-null int64 10 est_salary 9974 non-null float64 11 churn 9974 non-null int64 dtypes: float64(4), int64(6), object(2) memory usage: 935.2+ KB
Осталось 9974 строк.
Посмотрим тип данных у всех признаков.
df.dtypes
user_id int64 score float64 city object gender object age float64 equity int64 balance float64 products int64 credit_card int64 last_activity int64 est_salary float64 churn int64 dtype: object
Проверим столбец score и age, возможно, значения в столбах являются не вещественными, а целочисленными.
Для этого создадим функцию, которая после проверки дробной части значений столбца, не являющихся пропусками, на выходе будет выдавать являются ли значения вещественными либо целочисленными.
def isWhole(column):
for i in df[df[column].notna()][column]:
if i%1 != 0:
return print('в столбце вещественные значения')
return print('в столбце все значения целочисленные')
isWhole('score')
print()
isWhole('age')
в столбце все значения целочисленные в столбце все значения целочисленные
Изменим тип данных в столбцах age и score на целочисленный.
df[['age', 'score']] = df[['age', 'score']].astype(float).astype('int64')
df[['age', 'score']].dtypes
age int64 score int64 dtype: object
df.head()
| user_id | score | city | gender | age | equity | balance | products | credit_card | last_activity | est_salary | churn | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 183012 | 850 | Рыбинск | Ж | 25 | 1 | 59214.82 | 2 | 0 | 1 | 75719.14 | 1 |
| 1 | 146556 | 861 | Рыбинск | Ж | 37 | 5 | 850594.33 | 3 | 1 | 0 | 86621.77 | 0 |
| 2 | 120722 | 892 | Рыбинск | Ж | 30 | 0 | NaN | 1 | 1 | 1 | 107683.34 | 0 |
| 3 | 225363 | 866 | Ярославль | Ж | 51 | 5 | 1524746.26 | 2 | 0 | 1 | 174423.53 | 1 |
| 4 | 157978 | 730 | Ярославль | М | 34 | 5 | 174.00 | 1 | 1 | 0 | 67353.16 | 1 |
Создадим функцию, которая будет выводить описательную статистику и график ящик с усами.
def describe_attr(attribute):
# описательная статистика
print('Описательная статистика для значений {}:'.format(attribute) + '\n',
round(df[attribute].describe()))
# ящик с усами
plt.figure(figsize=(15,2))
sns.boxplot(x=attribute, data=df)
plt.title('Распределение значений {}'.format(attribute), fontsize=20, fontweight="bold")
plt.xlabel('{}'.format(attribute), fontsize=14, fontweight="bold")
plt.ylabel('', fontsize=14, fontweight="bold");
plt.show();
Посмотрим описательную статистику и построим график ящик с усами для age.
describe_attr('age')
Описательная статистика для значений age: count 9974.0 mean 43.0 std 12.0 min 18.0 25% 33.0 50% 40.0 75% 51.0 max 86.0 Name: age, dtype: float64
Клиенты в зависимости от возраста, лет:
Посмотрим описательную статистику и построим график ящик с усами для balance.
describe_attr('balance')
Описательная статистика для значений balance: count 7695.0 mean 827246.0 std 1980327.0 min 0.0 25% 295699.0 50% 524295.0 75% 980051.0 max 119113552.0 Name: balance, dtype: float64
Клиенты в зависимости от баланса:
Посмотрим описательную статистику по уровню дохода
describe_attr('est_salary')
Описательная статистика для значений est_salary: count 9974.0 mean 147787.0 std 139286.0 min 2546.0 25% 75252.0 50% 119627.0 75% 174500.0 max 1395064.0 Name: est_salary, dtype: float64
Клиенты в зависимости от дохода:
Посмотрим описательную статистику и построим график ящик с усами для products.
describe_attr('products')
Описательная статистика для значений products: count 9974.0 mean 2.0 std 1.0 min 0.0 25% 1.0 50% 2.0 75% 2.0 max 5.0 Name: products, dtype: float64
df.query('products == 0')
| user_id | score | city | gender | age | equity | balance | products | credit_card | last_activity | est_salary | churn | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 8939 | 147837 | 962 | Рыбинск | Ж | 79 | 3 | NaN | 0 | 0 | 0 | 25063.96 | 1 |
В банке один клиент без оформленных продуктов, что является аномалией.
Кроме того, у данного клиента отсутствует значение баланса, а также возраст 79, входящий в границу редких значений по возрасту.
Удалим данное аномальное значение.
df = df.query('products != 0').reset_index(drop=True)
df.shape
(9973, 12)
Проверим наличие явных дубликатов.
df.duplicated().sum()
0
Явные дубликаты отсутствуют.
Проверим наличие неявных дубликатов по столбцу user_id.
df['user_id'].duplicated().sum()
50
В столбце user_id встречаются 50 одинкаовых id.
Посмотрим встречаются ли дубликаты по user_id и city.
df[['user_id', 'city']].duplicated().sum()
0
В датафрейме отсутствуют записи с одинаковыми значениями в столбцах user_id и city.
Посмотрим встречаются ли дубликаты по user_id и churn.
df[['user_id', 'churn']].duplicated().sum()
0
Дубликаты отсутствуют.
Посмотрим встречаются ли дубликаты по user_id и age.
df[['user_id', 'age']].duplicated().sum()
5
В датафрейме 10 записей с одинаковыми значениями в столбцах user_id и age.
Посмотрим данные записи.
df.loc[df.duplicated(subset=['user_id','age'], keep = False)].sort_values(by='user_id')
| user_id | score | city | gender | age | equity | balance | products | credit_card | last_activity | est_salary | churn | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 5247 | 148826 | 895 | Ярославль | М | 32 | 5 | 1470273.14 | 2 | 0 | 0 | 118058.52 | 1 |
| 8350 | 148826 | 909 | Рыбинск | Ж | 32 | 0 | NaN | 3 | 1 | 0 | 28843.54 | 0 |
| 3270 | 155765 | 863 | Ярославль | М | 30 | 5 | 1036114.50 | 5 | 1 | 1 | 150744.50 | 1 |
| 5192 | 155765 | 923 | Рыбинск | М | 30 | 0 | NaN | 1 | 1 | 1 | 120296.60 | 0 |
| 3560 | 163207 | 853 | Рыбинск | М | 42 | 4 | 543839.62 | 1 | 1 | 1 | 105281.97 | 1 |
| 6779 | 163207 | 838 | Ярославль | Ж | 42 | 4 | 652776.60 | 2 | 1 | 1 | 97545.36 | 0 |
| 1358 | 211130 | 833 | Ярославль | М | 55 | 3 | 1231184.90 | 4 | 0 | 1 | 187758.38 | 1 |
| 3813 | 211130 | 918 | Рыбинск | Ж | 55 | 0 | NaN | 2 | 1 | 1 | 244202.04 | 0 |
| 8192 | 227795 | 840 | Рыбинск | М | 34 | 2 | 350768.03 | 1 | 1 | 0 | 102036.14 | 1 |
| 8481 | 227795 | 839 | Ярославль | М | 34 | 2 | 326593.14 | 2 | 1 | 0 | 103314.92 | 0 |
Как видно из среза данных, одинаковые id могут быть как у клиентов мужского пола так и у клиентов женского пола.
Исходя из вышенаписанного, нельзя сделать предположение, что записи с одинаковым id, возрастом и полом являются дубликатами.
Таким образом, наличие совпадения по user_id не является признаком дублирования.
Создадим функцию, которые будут выдавать статус клиента в зависимости от параметра.
# для churn
def replace_param_ch(param):
if param == 0:
return 'не отток'
else:
return 'отток'
# для credit_card
def replace_param_cr(param):
if param == 0:
return 'не оформлена'
else:
return 'оформлена'
# для last_activity
def replace_param_act(param):
if param == 0:
return 'не активный'
else:
return 'активный'
Создадим столбы customer_churn, cred_card, activity.
df['customer_churn'] = df['churn'].apply(replace_param_ch)
df['cred_card'] = df['credit_card'].apply(replace_param_cr)
df['activity'] = df['last_activity'].apply(replace_param_act)
df['gender'] = df['gender'].replace(['М', 'Ж'],['мужской', 'женский'])
Посмотрим внесенные изменения.
df.head()
| user_id | score | city | gender | age | equity | balance | products | credit_card | last_activity | est_salary | churn | customer_churn | cred_card | activity | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 183012 | 850 | Рыбинск | женский | 25 | 1 | 59214.82 | 2 | 0 | 1 | 75719.14 | 1 | отток | не оформлена | активный |
| 1 | 146556 | 861 | Рыбинск | женский | 37 | 5 | 850594.33 | 3 | 1 | 0 | 86621.77 | 0 | не отток | оформлена | не активный |
| 2 | 120722 | 892 | Рыбинск | женский | 30 | 0 | NaN | 1 | 1 | 1 | 107683.34 | 0 | не отток | оформлена | активный |
| 3 | 225363 | 866 | Ярославль | женский | 51 | 5 | 1524746.26 | 2 | 0 | 1 | 174423.53 | 1 | отток | не оформлена | активный |
| 4 | 157978 | 730 | Ярославль | мужской | 34 | 5 | 174.00 | 1 | 1 | 0 | 67353.16 | 1 | отток | оформлена | не активный |
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 9973 entries, 0 to 9972 Data columns (total 15 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 9973 non-null int64 1 score 9973 non-null int64 2 city 9973 non-null object 3 gender 9973 non-null object 4 age 9973 non-null int64 5 equity 9973 non-null int64 6 balance 7695 non-null float64 7 products 9973 non-null int64 8 credit_card 9973 non-null int64 9 last_activity 9973 non-null int64 10 est_salary 9973 non-null float64 11 churn 9973 non-null int64 12 customer_churn 9973 non-null object 13 cred_card 9973 non-null object 14 activity 9973 non-null object dtypes: float64(2), int64(8), object(5) memory usage: 1.1+ MB
round(100*(1 - len(df) / 10000), 2)
0.27
Потеря данных составила менее 0.5%.
При обработке данных:
Потеря данных при обработке данных составила менее 0.5%.
Определим доли ушедших и оставшихся клиентов по всему портфелю банка.
churn_rate_bank = round(len(df.query('churn == 1'))/ len(df), 3)
print('Доля ушедших клиентов', churn_rate_bank)
print('Доля оставшихся клиентов', round(1 - churn_rate_bank, 3))
Доля ушедших клиентов 0.182 Доля оставшихся клиентов 0.818
fig = px.pie(df['customer_churn'], names='customer_churn')
fig.update_layout(title='Доля ушедших и оставшихся клиентов по портфелю банка',
width=700,
height=400)
fig.show()
По всему банку доля оттока - 18.2%, оставшиеся клиенты - 81.8%.
Для непрерывных признаков
Создадим функцию, которая по признаку будет выводить описательную статистику и график ящик с усами для оттока и оставшихся клиентов.
В качестве параметра используется рассматриваемый признак.
def desсribe_attribute(attribute):
# описательная статистика
print('Описательная статистика для оттока:',
round(df.query('churn == 1')[attribute].describe())
)
print('')
print('Описательная статистика для лояльных клиентов:',
round(df.query('churn == 0')[attribute].describe())
)
# ящик с усами
plt.figure(figsize=(15,2))
sns.boxplot(x=attribute, y = 'customer_churn', data=df)
plt.title('Распределение значений {}'.format(attribute), fontsize=20, fontweight="bold")
plt.xlabel('{}'.format(attribute), fontsize=14, fontweight="bold")
plt.ylabel('', fontsize=14, fontweight="bold");
plt.show();
Создадим функцию, которая будет выводить гистограмму распределения по ушедшим и лояльным клиентам банка. В качестве параметров используются:
def hist_attribute(attribute, x_min, x_max, bins):
fig = px.histogram(
df,
x=attribute,
range_x=[x_min, x_max],
color='customer_churn',
nbins= bins,
histnorm='probability',
barmode = 'overlay',
title='Распределение по {}'.format(attribute),
)
fig.update_xaxes(title_text='{}'.format(attribute))
fig.update_yaxes(title_text='Частота')
fig.show()
Создадим функцию, которая будет определять долю оттока для интевала [min_value, max_value] по рассматриваемому признаку и сравнивать с показателем оттока по банку.
В качестве параметров используются:
Интервал [min_value, max_value] выбирается исходя из анализа гистограммы распределения ушедших и лояльных клиентов.
def churn_rate_attribute(atribute, min_value, max_value):
# определяется отток по признаку
churn_rate_atr = round(
len(df.loc[(df['churn'] == 1) & (df[atribute] >= min_value) & (df[atribute] <= max_value)]) /
len(df.loc[(df[atribute] >= min_value) & (df[atribute] <= max_value)])
, 2
)
print('Доля оттока клиентов составляет', churn_rate_atr)
print('')
# сравнение оттока по признаку с оттоком по банку
if churn_rate_bank < churn_rate_atr:
print('Доля оттока по рассматриваемому признаку превышает долю оттока по банку на', \
'{0:.0%}'.format(churn_rate_atr / churn_rate_bank - 1)
)
else:
print('Доля оттока по рассматриваемому признаку не превышает долю оттока по банку')
Для дискретных и категориальных признаков
Создадим функцию, которая будет:
def discrete_churn(attribute):
# создаем дф с определением коэффициентов оттока по признаку
churn_attr = df.query('churn == 1')[attribute].value_counts() / df[attribute].value_counts()
print('Коэффициенты оттока {}:'.format(attribute) + '\n', round(churn_attr, 3))
print()
# сбрасываем индексы и переименовываем столбцы
churn_attr = churn_attr.reset_index()
churn_attr.columns = ['attribute', 'churn_rate']
# сравниваем отток по признаку с оттоком по банку
for i in churn_attr.index:
if churn_attr['churn_rate'][i] > churn_rate_bank:
print('Превышение показателя оттока по банку', churn_attr['attribute'][i], \
'{0:.1%}'.format(churn_attr['churn_rate'][i] / churn_rate_bank-1))
# выводим график
plt.figure(figsize=(15,7))
sns.barplot(data = df, x= attribute, y = 'churn', ci= None)
plt.axhline(y=churn_rate_bank, linewidth=1, color='r', zorder=1)
plt.title('График оттока по {}'.format(attribute), fontsize=20, fontweight="bold")
plt.xlabel('{}'.format(attribute), fontsize=14, fontweight="bold")
plt.ylabel('Коэффициент оттока', fontsize=14, fontweight="bold");
Посмотрим описательную статистику и распределение выборки для score.
desсribe_attribute('score')
Описательная статистика для оттока: count 1818.0 mean 863.0 std 50.0 min 706.0 25% 828.0 50% 866.0 75% 898.0 max 1000.0 Name: score, dtype: float64 Описательная статистика для лояльных клиентов: count 8155.0 mean 845.0 std 68.0 min 642.0 25% 796.0 50% 848.0 75% 900.0 max 1000.0 Name: score, dtype: float64
У ушедших клиентов несущественно выше показатели по баллам кредитного скорринга.
Возможно, это обусловлено существенным различием в размерах выборок (оставшиеся клиены более чем в 4 раза превышают ушедших).
Построим график распределения ушедших и оставшихся клиентов в зависимости от баллов скорринга.
hist_attribute('score', 640, 1000, 50)
Удельный вес ушедших клиентов с высокими баллами скорринга (от 820 до 939) превышает удельный вес оставшихся клиентов.
Определим долю ушедших клиентов с баллами скорринга в диапазоне от 820 до 939 (включительно) балов и сравним с оттоком клиентов по банку.
churn_rate_attribute('score', 820, 939)
Доля оттока клиентов составляет 0.23 Доля оттока по рассматриваемому признаку превышает долю оттока по банку на 26%
Отток клиентов с баллами скорринга от 820 до 939 составляет 23%, что превышает уровень оттока клиентов по банку на 26%.
Определим показатель оттока по городам.
discrete_churn('city')
Коэффициенты оттока city: Ярославль 0.190 Рыбинск 0.163 Ростов 0.187 Name: city, dtype: float64 Превышение показателя оттока по банку Ярославль 4.5% Превышение показателя оттока по банку Ростов 2.8%
В Ярославле и Ростове показатель оттока клиентов составляет 19% и 18.7%, что превышает показатель оттока по банку на 4.5% и 2.8% соответственно.
discrete_churn('gender')
Коэффициенты оттока gender: мужской 0.237 женский 0.127 Name: gender, dtype: float64 Превышение показателя оттока по банку мужской 30.2%
Отток клиентов мужского пола составляет 23.7%, что превышает показатель по банку на 30.2%.
Посмотрим описательную статистику и распределение выборки.
desсribe_attribute('age')
Описательная статистика для оттока: count 1818.0 mean 41.0 std 11.0 min 18.0 25% 32.0 50% 39.0 75% 52.0 max 75.0 Name: age, dtype: float64 Описательная статистика для лояльных клиентов: count 8155.0 mean 43.0 std 12.0 min 18.0 25% 34.0 50% 40.0 75% 51.0 max 86.0 Name: age, dtype: float64
Описательная статистика и график ящик с усами не показывают существенного различия в возрасте ушедших и оставшихся клиентов в банке.
Построим график распределения ушедших и оставшихся клиентов в зависимости от возраста.
hist_attribute('age', 18, 86, 100)
Выделяются два интервала, для которых характерно - ушедшие клиенты превышает лояльных клиентов:
Определим долю ушедших клиентов на выявленных интервалах и сравним с оттоком клиентов по банку.
churn_rate_attribute('age', 25, 35)
Доля оттока клиентов составляет 0.22 Доля оттока по рассматриваемому признаку превышает долю оттока по банку на 21%
churn_rate_attribute('age', 50, 60)
Доля оттока клиентов составляет 0.26 Доля оттока по рассматриваемому признаку превышает долю оттока по банку на 43%
Отток клиентов по возрасту:
- от 25 до 35 лет составляет 22%, что превышает показатель по банку на 21%
- от 50 до 60 лет составляет 26%, что превышает показатель по банку на 43%
discrete_churn('equity')
Коэффициенты оттока equity: 0 0.035 1 0.120 2 0.158 3 0.208 4 0.251 5 0.301 6 0.360 7 0.462 8 0.353 9 0.538 Name: equity, dtype: float64 Превышение показателя оттока по банку 3 14.5% Превышение показателя оттока по банку 4 38.0% Превышение показателя оттока по банку 5 65.3% Превышение показателя оттока по банку 6 97.9% Превышение показателя оттока по банку 7 154.1% Превышение показателя оттока по банку 8 93.9% Превышение показателя оттока по банку 9 195.9%
Отток клиентов, у которых в собственности находятся более 2-х единиц имущества, превышает показатель оттока по банку (существенное превышение начинается у клиентов с собственностью более 3 ед. имущества).
Посмотрим описательную статистику и распределение выборки.
desсribe_attribute('balance')
Описательная статистика для оттока: count 1806.0 mean 1134458.0 std 2034446.0 min 6.0 25% 387482.0 50% 783909.0 75% 1348128.0 max 64866210.0 Name: balance, dtype: float64 Описательная статистика для лояльных клиентов: count 5889.0 mean 733032.0 std 1953952.0 min 0.0 25% 279655.0 50% 475410.0 75% 853826.0 max 119113552.0 Name: balance, dtype: float64
В балансах ушедших клиентов и лояльных банку наблюдаются различия:
Построим график распределения ушедших и оставшихся клиентов в зависимости от баланса.
На гистограмме отобразим клиентов с балансом до 6 млн., поскольку клиенты с большим балансом в банке встречаются редко.
hist_attribute('balance', 0, 6000000, 1500)
Клиенты с большим балансом (от 750 тыс.) склонны к оттоку.
Определим долю ушедших клиентов на выявленных интервалах и сравним с оттоком клиентов по банку.
churn_rate_attribute('balance', 750000, 150000000)
Доля оттока клиентов составляет 0.35 Доля оттока по рассматриваемому признаку превышает долю оттока по банку на 92%
Отток клиентов с балансом свыше 750тыс. составляет 35%, что превышает показатель по банку на 92%.
discrete_churn('products')
Коэффициенты оттока products: 1 0.071 2 0.191 3 0.286 4 0.633 5 0.421 Name: products, dtype: float64 Превышение показателя оттока по банку 2 5.0% Превышение показателя оттока по банку 3 57.2% Превышение показателя оттока по банку 4 247.8% Превышение показателя оттока по банку 5 131.3%
Отток клиентов, у которых оформлено более 2-х продуктов варьируется от 7% до 63% и превышает показатель оттока по банку в зависимости от количества оформленных продуктов на 5-247%%(существенное превышение начинается если в собственности более 3-х продуктов).
discrete_churn('cred_card')
Коэффициенты оттока cred_card: оформлена 0.148 не оформлена 0.256 Name: cred_card, dtype: float64 Превышение показателя оттока по банку не оформлена 40.4%
Отток клиентов без кредитных карт составляет 25.6%, что превышает банковский показатель на 40.4%.
discrete_churn('activity')
Коэффициенты оттока activity: активный 0.245 не активный 0.114 Name: activity, dtype: float64 Превышение показателя оттока по банку активный 34.5%
Отток 'активных' клиентов составляет 24.5%, что превышает банковский показатель на 34.5%.
Посмотрим описательную статистику и распределение выборки.
desсribe_attribute('est_salary')
Описательная статистика для оттока: count 1818.0 mean 148357.0 std 122932.0 min 10880.0 25% 83286.0 50% 125409.0 75% 176018.0 max 1263028.0 Name: est_salary, dtype: float64 Описательная статистика для лояльных клиентов: count 8155.0 mean 147675.0 std 142684.0 min 2546.0 25% 73446.0 50% 118228.0 75% 173961.0 max 1395064.0 Name: est_salary, dtype: float64
Существенных различий по доходам у ушедших и лояльных клиентов не наблюдается.
Среднее значение дохода у ушедших - 148.3тыс., у оставшихся - 147.6 тыс.
Медиана дохода у ушедших - 125.4тыс., у оставшихся - 118.2 тыс.
Построим график распределения ушедших и оставшихся клиентов в разрезе по доходам.
На гистограмме отобразим клиентов с доходом до 600 тыс., поскольку клиенты с большим доходов в банке встречаются редко.
hist_attribute('est_salary', 0, 500000, 100)
Клиенты с доходом от 100 тыс. до 220 тыс. более склонны к оттоку.
Определим долю ушедших клиентов на выявленных интервалах и сравним с оттоком клиентов по банку.
churn_rate_attribute('est_salary', 100000, 220000)
Доля оттока клиентов составляет 0.2 Доля оттока по рассматриваемому признаку превышает долю оттока по банку на 10%
Отток клиентов с доходом от 100тыс. до 220тыс. составляет 20%, что превышает показатель по банку на 10%.
corr_df = round(
df
[
[
'score',
'city',
'gender',
'age',
'equity',
'balance',
'products',
'credit_card',
'last_activity',
'est_salary',
'churn'
]
]
.phik_matrix(interval_cols = ['age', 'balance', 'est_salary', 'score'])
, 2
)
display(corr_df)
fig = px.imshow(corr_df, aspect="auto", color_continuous_scale='RdBu_r',
title='График матрицы корреляции')
fig.show()
| score | city | gender | age | equity | balance | products | credit_card | last_activity | est_salary | churn | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| score | 1.00 | 0.09 | 0.07 | 0.04 | 0.53 | 0.17 | 0.42 | 0.21 | 0.05 | 0.40 | 0.23 |
| city | 0.09 | 1.00 | 0.01 | 0.09 | 0.06 | 0.04 | 0.13 | 0.07 | 0.03 | 0.17 | 0.02 |
| gender | 0.07 | 0.01 | 1.00 | 0.29 | 0.06 | 0.05 | 0.07 | 0.20 | 0.01 | 0.14 | 0.22 |
| age | 0.04 | 0.09 | 0.29 | 1.00 | 0.04 | 0.02 | 0.14 | 0.16 | 0.09 | 0.36 | 0.18 |
| equity | 0.53 | 0.06 | 0.06 | 0.04 | 1.00 | 0.00 | 0.58 | 0.22 | 0.05 | 0.29 | 0.35 |
| balance | 0.17 | 0.04 | 0.05 | 0.02 | 0.00 | 1.00 | 0.00 | 0.00 | 0.00 | 0.37 | 0.02 |
| products | 0.42 | 0.13 | 0.07 | 0.14 | 0.58 | 0.00 | 1.00 | 0.27 | 0.11 | 0.22 | 0.26 |
| credit_card | 0.21 | 0.07 | 0.20 | 0.16 | 0.22 | 0.00 | 0.27 | 1.00 | 0.05 | 0.04 | 0.20 |
| last_activity | 0.05 | 0.03 | 0.01 | 0.09 | 0.05 | 0.00 | 0.11 | 0.05 | 1.00 | 0.04 | 0.26 |
| est_salary | 0.40 | 0.17 | 0.14 | 0.36 | 0.29 | 0.37 | 0.22 | 0.04 | 0.04 | 1.00 | 0.05 |
| churn | 0.23 | 0.02 | 0.22 | 0.18 | 0.35 | 0.02 | 0.26 | 0.20 | 0.26 | 0.05 | 1.00 |
По результатам анализа матрицы корреляций по всему датафрейму выявлена заметная корреляция между баллами кредитного скорринга и баллами собственности, а также между оформленными продуктами и и баллами собственности
В разрезе оттока клиентов умеренная корреляция между баллами собственности
corr_df_churn = round(
df.query('churn == 1')
[
[
'score',
'city',
'gender',
'age',
'equity',
'balance',
'products',
'credit_card',
'last_activity',
'est_salary'
]
]
.phik_matrix(interval_cols = ['age', 'balance', 'est_salary', 'score'])
, 2
)
display(corr_df_churn)
fig = px.imshow(corr_df_churn, aspect="auto", color_continuous_scale='RdBu_r',
title='График матрицы корреляции для оттока')
fig.show()
| score | city | gender | age | equity | balance | products | credit_card | last_activity | est_salary | |
|---|---|---|---|---|---|---|---|---|---|---|
| score | 1.00 | 0.08 | 0.08 | 0.29 | 0.45 | 0.46 | 0.34 | 0.30 | 0.28 | 0.48 |
| city | 0.08 | 1.00 | 0.02 | 0.12 | 0.04 | 0.02 | 0.17 | 0.04 | 0.03 | 0.12 |
| gender | 0.08 | 0.02 | 1.00 | 0.17 | 0.00 | 0.00 | 0.06 | 0.26 | 0.23 | 0.00 |
| age | 0.29 | 0.12 | 0.17 | 1.00 | 0.17 | 0.00 | 0.22 | 0.09 | 0.19 | 0.39 |
| equity | 0.45 | 0.04 | 0.00 | 0.17 | 1.00 | 0.00 | 0.24 | 0.19 | 0.15 | 0.09 |
| balance | 0.46 | 0.02 | 0.00 | 0.00 | 0.00 | 1.00 | 0.00 | 0.00 | 0.00 | 0.70 |
| products | 0.34 | 0.17 | 0.06 | 0.22 | 0.24 | 0.00 | 1.00 | 0.32 | 0.10 | 0.14 |
| credit_card | 0.30 | 0.04 | 0.26 | 0.09 | 0.19 | 0.00 | 0.32 | 1.00 | 0.14 | 0.07 |
| last_activity | 0.28 | 0.03 | 0.23 | 0.19 | 0.15 | 0.00 | 0.10 | 0.14 | 1.00 | 0.06 |
| est_salary | 0.48 | 0.12 | 0.00 | 0.39 | 0.09 | 0.70 | 0.14 | 0.07 | 0.06 | 1.00 |
По результатам анализа матрицы корреляций для ушедших клиентов выявлена высокая корреляция для баланса и дохода
Сгруппируем данные в разрезе отток/не отток по дискретным и категориальным признакам и определим моду.
df_gr_mode = (
df
.groupby('customer_churn')
[
[
'city',
'gender',
'equity',
'products',
'cred_card',
'activity'
]
]
.agg(pd.Series.mode)
.reset_index(drop=True)
.T
.reset_index()
.rename(columns = {'index':'attribute', 0:'not_churn', 1:'churn'})
)
df_gr_mode
| attribute | not_churn | churn | |
|---|---|---|---|
| 0 | city | Ярославль | Ярославль |
| 1 | gender | женский | мужской |
| 2 | equity | 0 | 5 |
| 3 | products | 2 | 2 |
| 4 | cred_card | оформлена | оформлена |
| 5 | activity | не активный | активный |
Создадаим сводную таблицу в разрезе отток/ не отток по непрерывным признакам, определим среднее и рассчитаем относительную разницу.
df_gr_mean = (
df
.pivot_table(
index='customer_churn',
values=
[
'balance',
'est_salary',
'score',
'age'
]
, aggfunc=
{
'balance':'mean',
'est_salary':'mean',
'score':'mean',
'age':'mean'
}
)
.reset_index(drop=True)
.T
.reset_index()
.rename(columns = {'index':'attribute', 0:'not_churn', 1:'churn'})
)
df_gr_mean = round(df_gr_mean)
df_gr_mean['relative_diff'] = round(df_gr_mean['churn'] / df_gr_mean['not_churn'], 2)
df_gr_mean
| attribute | not_churn | churn | relative_diff | |
|---|---|---|---|---|
| 0 | age | 43.0 | 41.0 | 0.95 |
| 1 | balance | 733032.0 | 1134458.0 | 1.55 |
| 2 | est_salary | 147675.0 | 148357.0 | 1.00 |
| 3 | score | 845.0 | 863.0 | 1.02 |
Для признаков, у которых много редких значений посчитаем медиану, чтобы посмотреть ассиметрию распределений.
df_gr_median = (
df
.pivot_table(
index='customer_churn',
values=
[
'balance',
'est_salary',
#'score',
#'age'
]
, aggfunc=
{
'balance':['mean','median'],
'est_salary':['mean','median'],
#'score':'median',
#'age':'mean'
}
)
.reset_index(drop=True)
)
df_gr_median = round(df_gr_median)
df_gr_median
| balance | est_salary | |||
|---|---|---|---|---|
| mean | median | mean | median | |
| 0 | 733032.0 | 475410.0 | 147675.0 | 118228.0 |
| 1 | 1134458.0 | 783909.0 | 148357.0 | 125409.0 |
Средние значения balance и est_salary больше, чем медиана, значит распределение ассимитрично вправо.
Объединим полученные таблицы.
customer_profile = pd.concat([df_gr_mean,df_gr_mode], ignore_index= True ).fillna('-')
customer_profile
| attribute | not_churn | churn | relative_diff | |
|---|---|---|---|---|
| 0 | age | 43.0 | 41.0 | 0.95 |
| 1 | balance | 733032.0 | 1134458.0 | 1.55 |
| 2 | est_salary | 147675.0 | 148357.0 | 1.0 |
| 3 | score | 845.0 | 863.0 | 1.02 |
| 4 | city | Ярославль | Ярославль | - |
| 5 | gender | женский | мужской | - |
| 6 | equity | 0 | 5 | - |
| 7 | products | 2 | 2 | - |
| 8 | cred_card | оформлена | оформлена | - |
| 9 | activity | не активный | активный | - |
Для клиентов, покинувших банк характерно:
Сформулируем нулевую и альтернативную гипотезу:
Посмотрим размер выборок ушедших и лояльных клиентов.
print('размер выборки для оттока', len(df.query('churn == 1')))
print('размер выборки для оставшихся клиентов', len(df.query('churn == 0')))
размер выборки для оттока 1818 размер выборки для оставшихся клиентов 8155
Создадим функию сравнивающую дисперсию выборок ratio_var(selection1, selection2, n).
В качестве параметров передаются сравниваемые выборки gen_pop1 и gen_pop2, n - вещественное число, с которым будет сравнение отшения дисперсий выборок - отклонение, при котором выборки будут считаться равными.
def ratio_var(selection1, selection2, n):
if selection1.dropna().var() >= selection2.dropna().var():
results = (selection1.dropna().var() / selection2.dropna().var() - 1) * 100
else:
results = (selection2.dropna().var() / selection1.dropna().var() - 1) * 100
if results < n:
print('Отношение дисперсий выборок составляет', round(results, 2))
print('В качестве параметра equal_var передается True')
else:
print('Отношение дисперсий выборок составляет', round(results, 2))
print('В качестве параметра equal_var передается False')
Проверим равенство дисперсий выборок.
ratio_var(
df.query('churn == 1')['est_salary'],
df.query('churn == 0')['est_salary'],
0.05
)
Отношение дисперсий выборок составляет 34.72 В качестве параметра equal_var передается False
Дисперсии выборок различаются.
Чтобы проверить гипотезу о равенстве среднего 2-х совокупностей применим метод scipy.stats.ttest_ind(), поскольку условия для проведения ttesta выполняются:
В качестве параметра equal_var передадим False, поскольку дисперсия и размеры выборок различаются.
Уровень значимости зададим равным 5%.
alpha = .05
results = st.ttest_ind(
df.query('churn == 1')['est_salary'].dropna(),
df.query('churn == 0')['est_salary'].dropna(),
equal_var = False
)
print('p-значение:', results.pvalue)
if results.pvalue < alpha:
print('Отвергаем нулевую гипотезу')
else:
print('Не получилось отвергнуть нулевую гипотезу')
p-значение: 0.8354803526840116 Не получилось отвергнуть нулевую гипотезу
Полученное p-значение больше уровня значимости, следовательно нет оснований отвергнуть нулевую гипотезу, т.е. значимого расхождения между средними доходами ушедших и лояльных клиентов не выявлено.
Таким образом, средние доходы ушедших и лояльных клиентов могут быть одинаковыми, либо не достаточно данных для доказательств того, что средние доходы различаются.
Результаты проведенного t-теста показывают не обоснованность утверждения, сформулированного в гипотезе о различии в доходах.
Сформулируем нулевую и альтернативную гипотезу:
Посмотрим размер выборок ушедших клиентов и мужского пола и лояльных клиентов женского пола.
print(len(df.query('churn == 1 and gender == "женский"')['balance']))
print(len(df.query('churn == 0 and gender == "мужской"')['balance']))
635 3808
Проверим равенство дисперсий выборок.
ratio_var(
df.query('churn == 1 and gender == "женский"')['balance'],
df.query('churn == 0 and gender == "мужской"')['balance'],
0.05
)
Отношение дисперсий выборок составляет 787.02 В качестве параметра equal_var передается False
Дисперсии выборок различаются.
Чтобы проверить гипотезу о равенстве среднего 2-х совокупностей применим метод scipy.stats.ttest_ind(), поскольку условия для проведения ttesta выполняются:
В качестве параметра equal_var передадим False, поскольку дисперсия и размеры выборок различаются.
Уровень значимости зададим равным 5%.
alpha = .05
results = st.ttest_ind(
df.query('churn == 1 and gender == "женский"')['balance'].dropna(),
df.query('churn == 0 and gender == "мужской"')['balance'].dropna(),
equal_var = False,
alternative = 'greater'
)
print('p-значение:', results.pvalue)
if results.pvalue < alpha:
print('Отвергаем нулевую гипотезу')
else:
print('Не получилось отвергнуть нулевую гипотезу')
p-значение: 0.0040340982510890565 Отвергаем нулевую гипотезу
Полученное p-значение гораздо меньше уровня значимости, следовательно отвергаем нулевую гипотезу о равенстве средних двух генеральных совокупностей в пользу альтернативной гипотезы.
Таким образом, у ушедших клиентов женского пола средний баланс выше, чем у лояльных клиентов мужского.
Таким образом, результаты проведенного t-теста показывают на обоснованность утверждения, сформулированного в гипотезе о различии в балансах.
Сформулируем нулевую и альтернативную гипотезу:
Посмотрим размер выборок.
print(len(df.query('churn == 0 and gender == "женский" and age <= 35')['score']))
print(len(df.query('churn == 1 and gender == "мужской" and age > 35')['score']))
1149 704
Проверим равенство дисперсий выборок.
ratio_var(
df.query('churn == 0 and gender == "женский" and age <= 35')['score'],
df.query('churn == 1 and gender == "мужской" and age > 35')['score'],
0.05
)
Отношение дисперсий выборок составляет 51.78 В качестве параметра equal_var передается False
Чтобы проверить гипотезу о равенстве среднего 2-х совокупностей применим метод scipy.stats.ttest_ind(), поскольку условия для проведения ttesta выполняются:
В качестве параметра equal_var передадим False, поскольку дисперсия и размеры выборок различаются.
Уровень значимости зададим равным 5%.
alpha = .05
results = st.ttest_ind(
df.query('churn == 1 and gender == "женский" and age <= 35')['score'].dropna(),
df.query('churn == 0 and gender == "мужской" and age > 35')['score'].dropna(),
equal_var = False,
alternative = 'greater'
)
print('p-значение:', results.pvalue)
if results.pvalue < alpha:
print('Отвергаем нулевую гипотезу')
else:
print('Не получилось отвергнуть нулевую гипотезу')
p-значение: 2.059772170124498e-24 Отвергаем нулевую гипотезу
Полученное p-значение гораздо меньше уровня значимости, следовательно отвергаем нулевую гипотезу о равенстве средних двух генеральных совокупностей в пользу альтернативной гипотезы.
Это означает, что средний балл у ушедших клиентов женского пола до 35 лет (включительно) выше чем у лояльных клиентов мужского пола старше 35 лет.
Таким образом, результаты проведенного t-теста показывают на обоснованность утверждения, сформулированного в гипотезе о различии в баллах скоринга.
Коэффициент оттока клиентов по всему банку составляет 18.2%.
Отток клиентов в разрезе по признакам:
По результатам анализа матрицы корреляций по всему датафрейму выявлена заметная корреляция между баллами кредитного скорринга и баллами собственности, а также между оформленными продуктами и и баллами собственности. В разрезе оттока клиентов умеренная корреляция между баллами собственности.
По результатам анализа матрицы корреляций для ушедших клиентов выявлена высокая корреляция для баланса и дохода.
Портрет клиента, наиболее склонного к оттоку:
Проверены гипотезы:
результаты проведенного t-теста показывают не обоснованность утверждения, сформулированного в гипотезе 'между ушедшими и оставшимися клиентами существует различие в доходах'. Гипотеза опровергнута, что означает значимого расхождения между средними доходами ушедших и лояльных клиентов не выявлено. Таким образом, средние доходы ушедших и лояльных клиентов могут быть одинаковыми, либо не достаточно данных для доказательств того, что средние доходы различаются.
результаты проведенного t-теста показывают обоснованность утверждения, сформулированного в гипотезе 'средний баланс у ушедших клиентов женского пола выше, чем у лояльных клиентов мужского', поскольку гипотеза о равенстве средних значений была отвергнута в пользу альтернативной гипотезы. Таким образом, у ушедших клиентов женского пола средний баланс выше, чем у лояльных клиентов мужского.
результаты проведенного t-теста показывают обоснованность утверждения, сформулированного в гипотезе 'средний балл скорринга у ушедших клиентов женского пола младше 35 лет (включительно) выше, чем у лояльных клиентов мужского пола возрастом старше 35 лет', поскольку гипотеза о равенстве средних значений была отвергнута в пользу альтернативной гипотезы. Таким образом, средний балл у ушедших клиентов женского пола до 35 лет (включительно) выше, чем у лояльных клиентов мужского пола старше 35 лет.
Проведем сегментацию признаков по интервалам значений признака, наиболее склонных к оттоку.
Создадим функцию сегментации признака по интервалу def seсtor(atribute, interval, min_value, max_value).
Функции передаются параметры: признак, интервал признака (строчное выражение), минимальное значение интервала, максимальное значение интервала).
Результатом будет дф из одной строки со значениями (наименование признака, интревал признака, размер сегмента, размер сегмента оттока, коэффициент оттока, отношение коэффициента оттока к показателю по банку).
def seсtor(atribute, interval, min_value, max_value):
dict = {}
dict['attribute'] = atribute
dict['interval'] = interval
dict['size_churn'] = len(df.loc[(df['churn'] == 1) & (df[atribute] >= min_value) & (df[atribute] <= max_value)])
dict['size'] = len(df.loc[(df[atribute] >= min_value) & (df[atribute] <= max_value)])
dict['churn_rate'] = round(dict['size_churn'] / dict['size'],2)
dict['diff_%'] = round(100* (dict['churn_rate'] / churn_rate_bank - 1))
ds = pd.DataFrame.from_dict(dict, orient='index').T
ds = ds[['attribute', 'interval', 'size', 'size_churn', 'churn_rate', 'diff_%']]
return ds
Создадим дф с сегментами по каждому признаку.
score = seсtor('score', 'от 820 до 939', 820, 939)
balance = seсtor('balance', 'от 750тыс.', 750000, 150000000)
est_salary = seсtor('est_salary', 'от 100тыс. до 220тыс.', 100000, 220000)
age_30 = seсtor('age', 'от 25 до 35', 25, 35)
age_50 = seсtor('age', 'от 50 до 60', 50, 60)
products = seсtor('products', 'от 3 до 5', 3, 5)
equity = seсtor('equity', 'от 3 до 9', 3, 9)
seсtor_churn = pd.concat(
[
score,balance,
est_salary,
age_30, age_50,
products,
equity
],
ignore_index= True
)
seсtor_churn.sort_values(by='diff_%', ascending=False, ignore_index = True)
| attribute | interval | size | size_churn | churn_rate | diff_% | |
|---|---|---|---|---|---|---|
| 0 | products | от 3 до 5 | 1531 | 605 | 0.4 | 120 |
| 1 | balance | от 750тыс. | 2700 | 941 | 0.35 | 92 |
| 2 | age | от 50 до 60 | 1768 | 468 | 0.26 | 43 |
| 3 | equity | от 3 до 9 | 5573 | 1469 | 0.26 | 43 |
| 4 | score | от 820 до 939 | 5980 | 1371 | 0.23 | 26 |
| 5 | age | от 25 до 35 | 3055 | 665 | 0.22 | 21 |
| 6 | est_salary | от 100тыс. до 220тыс. | 4708 | 963 | 0.2 | 10 |
Определим 3 сегмента с более точными характеристиками, у которых размер более 500 и показатель оттока превышает банковский.
Сегмент 1. Клиенты, баланс которых свыше 750000.
seсtor('balance', 'от 750тыс.', 750000, 150000000)
| attribute | interval | size | size_churn | churn_rate | diff_% | |
|---|---|---|---|---|---|---|
| 0 | balance | от 750тыс. | 2700 | 941 | 0.35 | 92 |
Сегмент 2. Клиенты, доход которых от 100тыс. до 220тыс. и баллы кредитного скорринга выше 820.
print('размер сегмента',
len(df.query('churn == 1 & 100000 <= est_salary <= 220000 & score >= 820')) )
print('показатель оттока',
round(
len(df.query('churn == 1 & 100000 <= est_salary <= 220000 & score >= 820 ')) /
len(df.query('100000 <= est_salary <= 220000 & score >= 820 '))
,2)
)
print('превышение показателя по банку',
round(
len(df.query('churn == 1 & 100000 <= est_salary <= 220000 & score >= 820 ')) /
len(df.query('100000 <= est_salary <= 220000 & score >= 820 ')) /
churn_rate_bank
,2)
)
размер сегмента 808 показатель оттока 0.25 превышение показателя по банку 1.36
Сегмент 3. Клиенты возрастом от 25 до 35 и статус активный
print('размер сегмента', len(df.query('churn == 1 & 25 <= age <= 35 & last_activity == 1')))
print('показатель оттока',
round(
len(df.query('churn == 1 & 25 <= age <= 35 & last_activity == 1')) /
len(df.query('25 <= age <= 35 & last_activity == 1'))
,2)
)
print('превышение показателя по банку',
round(
len(df.query('churn == 1 & 25 <= age <= 35 & last_activity == 1')) /
len(df.query('25 <= age <= 35 & last_activity == 1'))/
churn_rate_bank
,2)
)
размер сегмента 527 показатель оттока 0.32 превышение показателя по банку 1.75
Проранжируем сегменты по степени приоритезации в зависимости от показателя оттока.
По результатам исследования был составлен портрет клиентa в разрезе оттока:
Были выявлены сегменты клиентов с коэффициентом оттока превышающим показатель оттока по банку:
Проверка гипотез показала:
результаты проведенного t-теста показывают не обоснованность утверждения, сформулированного в гипотезе 'между ушедшими и оставшимися клиентами существует различие в доходах'. Гипотеза опровергнута, что означает значимого расхождения между средними доходами ушедших и лояльных клиентов не выявлено. Таким образом, средние доходы ушедших и лояльных клиентов могут быть одинаковыми, либо не достаточно данных для доказательств того, что средние доходы различаются.
результаты проведенного t-теста показывают обоснованность утверждения, сформулированного в гипотезе 'средний баланс у ушедших клиентов женского пола выше, чем у лояльных клиентов мужского', поскольку гипотеза о равенстве средних значений была отвергнута в пользу альтернативной гипотезы. Таким образом, у ушедших клиентов женского пола средний баланс выше, чем у лояльных клиентов мужского.
результаты проведенного t-теста показывают обоснованность утверждения, сформулированного в гипотезе 'средний балл скорринга у ушедших клиентов женского пола младше 35 лет (включительно) выше, чем у лояльных клиентов мужского пола возрастом старше 35 лет', поскольку гипотеза о равенстве средних значений была отвергнута в пользу альтернативной гипотезы. Таким образом, средний балл у ушедших клиентов женского пола до 35 лет (включительно) выше, чем у лояльных клиентов мужского пола старше 35 лет.
Для клиентов, баланс которых свыше 750тыс.:
Для клиентов возрастом от 25 до 35 и статус активный
Для клиентов, доход которых от 100тыс. до 220тыс. и баллы кредитного скорринга выше 820: